package seedpod.model;

/**
 * Copyright 2009 University of Washington This code is released under the GPL version 3 - with a copy of the license
 * included
 * 
 * @author Maya Hao Li
 */
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;

import seedpod.exceptions.SeedpodException;
import seedpod.kb2db.ClsMap;
import seedpod.kb2db.ModelMap;
import seedpod.kb2db.SlotMap;
import seedpod.rdb.PManager;
import seedpod.rdb.PersistenceDO;
import seedpod.rdb.RdbSql;
import seedpod.rdb.SqlUtil;

/**
 * A generic Seedpod Data object object. the object is not a reified attribute object. It may contain data crossing
 * multiple database tables
 */

public class SeedpodDO implements PersistenceDO {

	private HashMap<String, AVPair> _avPairs;
	protected String _classType;
	protected PManager _manager;
	protected Integer _oid = UNKNOWN_OID;
	protected Thing _thingInstance;

	public SeedpodDO(Integer oid, PManager manager) throws NoObjectFoundException {
		_oid = oid;
		_manager = manager;
		_thingInstance = new Thing( _oid, _manager );
		init();
	}

	public SeedpodDO(String classType, Integer oid, PManager manager) throws NoObjectFoundException {
		_classType = classType;
		_oid = oid;
		_manager = manager;
		_thingInstance = _oid == 0 ? new Thing( _classType, _oid, Thing.STATUS_INVALID, _manager ) : new Thing( _classType, _oid, Thing.STATUS_INVALID, _manager );
		init();
	}

	public SeedpodDO(String classType, PManager manager) throws NoObjectFoundException {
		_classType = classType;

		_oid = UNKNOWN_OID;
		_manager = manager;
		_thingInstance = new Thing( _classType, _oid, _manager );
		init();
	}

	/**
	 * Create an object of _class type
	 */
	public boolean create() throws SeedpodException {
		int result = 0;
		try {
			if ( !_thingInstance.create() ) throw new SeedpodException( "Problem create new " + _classType );

			_oid = _thingInstance.getOid();
			String sql = getSqlInsert();
			System.err.println( "CREATE SQL: " + sql );

			result = _manager.getRdbConnection().executeUpdate( sql );
			_thingInstance = new Thing( _classType, _oid, Thing.STATUS_SAVED, _manager );

			System.err.println( "create SeedpodDO success?: " + result );
		} catch (SQLException e) {
			_thingInstance.delete();
			throw new SeedpodException( e, "SeedpodDO create err: " + e.getMessage() );
		}

		return result != 0;
	}

	// submits the sql update.
	protected int dbUpdate( String sql ) throws SeedpodException {
		int queryResult = 0;
		try {
			queryResult = _manager.getRdbConnection().executeUpdate( sql );
		} catch (SQLException e) {
			throw new SeedpodException( e, "SQLException in ThingInstance() update" );
		}
		return queryResult;
	}

	public boolean delete() throws SeedpodException {
		return _thingInstance.delete();
	}

	public SlotMap getAttribute( String aName ) {
		SlotMap smap = null;
		ClsMap cmap = getMetaCls();
		smap = cmap.getSlotMap( aName );
		return smap;
	}

	public AVPair getAttributeValue( String attName ) {
		SlotMap att = getAttribute( attName );
		if ( att != null )
			return getAVPair( att );
		else return null;
	}

	public ArrayList<AVPair> getAttributeValues() {
		return new ArrayList<AVPair>( _avPairs.values() );
	}

	public AVPair getAVPair( SlotMap att ) {
		String attName = att.getName();
		AVPair binder;
		if ( _avPairs.containsKey( attName ) )
			binder = _avPairs.get( attName );
		else {
			binder = new AVPair( this, att );
			_avPairs.put( attName, binder );
		}
		return binder;
	}

	public String getDefaultLabel() {
		return _classType + "(" + _oid + ")";
	}

	public String getLabel() {
		ClsMap cmap = _manager.getModelMap().getClsMap( _classType );
		String label = "";
		String[] patterns = cmap.getBrowserPatternArray();

		for ( String p : patterns ) {
			if ( p.startsWith( "VAL" ) ) p = p.substring( p.indexOf( "(" ) + 1, p.indexOf( ")" ) );

			if ( cmap.hasAttribute( p ) ) {
				SlotMap key = cmap.getSlotMap( p );

				if ( key.isRelationship() )
					label += "->" + key.getName(); // TODO getRelationshipValue( key
				// ).getTargetObject().label();
				else {
					String tmp = getAttributeValue( p ).getStringValue();
					label += (tmp == null ? "" : tmp) + "  ";
				}
			} else if ( p.equalsIgnoreCase( "ID" ) )
				label += _oid.toString();
			else label += p + " ";
		}

		if ( label.trim().length() == 0 ) return getDefaultLabel();

		return label;
	}

	public PManager getManager() {
		return _manager;
	}

	public ClsMap getMetaCls() {
		ModelMap map = _manager.getModelMap();
		ClsMap metaCls = map.getClsMap( _classType );
		return metaCls;
	}

	// calls getSlotMaps for now. Reimplement to return Collection<Attribute>
	public Collection<SlotMap> getMetaSlots() {
		return getSlotMaps();
	}

	public Integer getOid() {
		return _oid;
	}

	public Collection<SlotMap> getSlotMaps() {
		ClsMap clsMap = getMetaCls();
		return clsMap != null ? clsMap.getSlotMaps() : null;
	}

	protected String getSqlInsert() {
		ClsMap cmap = _manager.getModelMap().getClsMap( _classType );
		String keyString = SqlUtil.QQ( "ID" ) + SqlUtil.C;
		String valString = _oid + SqlUtil.C;

		for ( AVPair avpair : _avPairs.values() ) {
			SlotMap sm = avpair.getAttribute();

			if ( avpair == null || !sm.isAssociated() ) continue;

			String tmpVal = RdbSql.toSqlValString( avpair.getValue(), sm.getRdbValueType() );
			String tmpKey = SqlUtil.QQ( sm.getName() );

			keyString += tmpKey + SqlUtil.C;
			valString += tmpVal + SqlUtil.C;
		}

		keyString = keyString.trim();
		keyString = keyString.substring( 0, keyString.length() - 1 );
		valString = valString.trim();
		valString = valString.substring( 0, valString.length() - 1 );

		String insertStmt = RdbSql.INSERT_INTO + SqlUtil.SP + SqlUtil.QQ( cmap.getRelationName() );
		insertStmt += SqlUtil.PARAN( keyString ) + SqlUtil.NL + " VALUES " + SqlUtil.PARAN( valString ) + SqlUtil.SC;
		return insertStmt;
	}

	public boolean getSqlSelectLike( SeedpodDO object ) throws SeedpodException {
		String filter = "";
		ClsMap cmap = getMetaCls();

		if ( cmap == null ) System.err.println( "retrievelike() can not find metacls" );

		int attCt = 0;

		for ( AVPair avpair : object.getAttributeValues() ) {
			SlotMap attribute = avpair.getAttribute();
			String value = avpair.getStringValue();

			if ( value == null || value.equalsIgnoreCase( "null" ) || value.length() == 0 ) continue;

			if ( attCt > 0 ) filter += SqlUtil.SP + RdbSql.AND + SqlUtil.SP;

			filter += toFullName( cmap, attribute ) + RdbSql.EQUALS + SqlUtil.Q( value );
			attCt++;
		}

		System.err.println( "SeedpodDataObject.retrieveLike filter " + filter );
		return retrieve( filter );

	}

	protected String getSqlUpdate() {
		ClsMap cMap = _manager.getModelMap().getClsMap( _classType );
		String pvStr = "";

		// generate a string of attribute and values
		for ( AVPair p : getAttributeValues() ) {
			SlotMap sm = p.getAttribute();
			if ( sm == null || !sm.isAssociated() ) continue;
			pvStr += SqlUtil.QQ( sm.getName() ) + RdbSql.EQUALS;

			if ( sm.isRelationship() )
				System.out.println( sm.getName() + " = " + RdbSql.toSqlValString( p.getValue(), sm.getRdbValueType() ) );

			pvStr += RdbSql.toSqlValString( p.getValue(), sm.getRdbValueType() ) + SqlUtil.C;
		}

		pvStr = pvStr.trim().substring( 0, pvStr.lastIndexOf( SqlUtil.C ) );

		String sql = RdbSql.UPDATE + SqlUtil.QQ( cMap.getTableName() ) + RdbSql.SET;
		sql += pvStr + SqlUtil.SP;
		sql += RdbSql.WHERE + SqlUtil.QQ( SeedpodModel.RdbSlot.THING_ID ) + RdbSql.EQUALS + _oid;

		System.err.println( "updateRowSql: " + sql );
		return sql;
	}

	public String getState() {
		return _thingInstance.getState();
	}

	/**
	 * initializes the attribute value binder
	 */
	private void init() {
		// initialize Attribute-Value binders
		_avPairs = new HashMap<String, AVPair>();
		Collection<SlotMap> slotMaps = _manager.getModelMap().getClsSlotMaps( _classType );
		for ( SlotMap sm : slotMaps ) {
			AVPair avBinder = new AVPair( this, sm );
			_avPairs.put( sm.getName(), avBinder );
		}
	}

	public boolean isDeleted() {
		return _thingInstance.isDeleted();
	}

	public boolean isDirty() {
		return _thingInstance.isDirty();
	}

	public boolean isPersistent() {
		return _thingInstance.isPersistent();
	}

	/**
	 * Load a row of result set into the object. This has only one row of result set.
	 */
	public void load( ResultSet rs ) throws SeedpodException, SQLException {
		System.err.println( "... start loading result set into SeedpodDO ... " );
		if ( rs == null ) {
			makeTransient();
			throw new SeedpodException( "No result set. " );
		}

		// get primary key for the object.
		ClsMap cmap = getMetaCls();
		if ( cmap.hasPrimaryKey() ) {
			String pkstr = cmap.getPrimaryKey();
			try {
				int pkindex = rs.findColumn( pkstr );
				_oid = rs.getInt( pkindex );
			} catch (SQLException sqle) {
				System.err.println( " Not primary key in query. " );
			}
		}

		// iterate through each value and bind attribute to value, or create relationships.
		Collection<SlotMap> attributes = getSlotMaps();
		Iterator<SlotMap> attIter = attributes.iterator();
		while (attIter.hasNext()) {
			SlotMap att = attIter.next();

			// 1-to-many relationship
			if ( att.isRelationship() && att.isMultiple() ) {
				InstanceRelationship rel = new InstanceRelationship( att, this );
				rel.init1ToManyRelationship( getOid() );
				setAttributeValue( att, rel );
			}
			// 1-to-1 relationship
			else if ( att.isRelationship() && !att.isMultiple() ) {
				InstanceRelationship rel = new InstanceRelationship( att, this );
				Integer targetOid = rs.getInt( att.getRdbTargetName() );
				if ( targetOid != null ) rel.addRelationship( getOid(), targetOid );
				setAttributeValue( att, rel );
			}
			// 1-to-many simple type values
			else if ( att.isMultiple() ) {
				ValueSetRelationship valSet = new ValueSetRelationship( att, this );
				valSet.initalize();
				setAttributeValue( att, valSet );
			}
			// simple type values
			else {
				Object value = rs.getObject( att.getRdbAttributeName() );
				setAttributeValue( att, value );
			}
		}
	}

	// save the object to the database. throw seedpodException if save fails
	public void makePersist() throws SeedpodException {
		// if no oid, create the object in lure of saving over and existing one
		if ( _oid == null || _oid == SeedpodDO.UNKNOWN_OID ) {
			create();
			return;
		}

		// retrieve the instance if necessary
		if ( _thingInstance == null ) _thingInstance.retrieveByOid();

		try {
			_manager.getRdbConnection().executeUpdate( getSqlUpdate() );
			_thingInstance.makePersist();
			_thingInstance.update();
		} catch (SQLException sqle) {
			_thingInstance.markInvalid();
			throw new SeedpodException( sqle, "UPDATE err: " + sqle.getMessage() );
		}

	}

	public void makeTransient() {
		_thingInstance.markInvalid();
	}

	public void markDirty() {
		_thingInstance.markDirty();
	}

	/**
	 * Converts user entry data into appropriate format. especially this is to reformat relationship data type
	 * 
	 * @param userEntry
	 * @return
	 */
	private Object reformatUserEntry( SlotMap attribute, Object userEntry ) {
		if ( attribute.isRelationship() )
			return InstanceRelationship.createRelation( attribute, this, userEntry );
		// TODO if attribute is multiVal
		else return userEntry;
	}

	/**
	 * retrieves the data using queryFilter
	 * 
	 * @throws SeedpodException
	 * @throws SQLException
	 */
	public boolean retrieve( String queryFilter ) throws SeedpodException {
		boolean success = false;
		ClsMap cmap = getMetaCls();
		StringBuffer query;
		ResultSet rs;

		query = RdbSql.selectSql( cmap.getName(), null, queryFilter );
		System.err.println( "retrieve query=" + query );

		// execute query
		try {
			rs = _manager.getRdbConnection().executeQuery( query.toString() );

			if ( rs == null )
				success = false;
			else {
				rs.next();
				load( rs );
				success = true;
				_thingInstance = new Thing( _classType, _oid, _manager );
			}
		} catch (SQLException e) {
			success = false;
			throw new SeedpodException( e, "retrieve() error: " + query.toString() );
		} catch (NoObjectFoundException e) {
			throw new SeedpodException( e, "No object found for oid " + _oid );
		}

		return success;
	}

	public void retrieveById() throws SeedpodException {
		String filterByOid = "";
		ClsMap metaCls = getMetaCls();

		if ( _oid == null || _oid == 0 )
			throw new SeedpodException( "No valid ID in SeedpodDataObject.retrieveByOid()" );

		filterByOid = SqlUtil.QQ( metaCls.getPrimaryKey() ) + RdbSql.EQUALS + _oid.toString();

		boolean success = retrieve( filterByOid );

		if ( !success ) {
			_thingInstance.markInvalid();
			throw new SeedpodException( "No " + metaCls.getName() + " found with id " + _oid );
		}
	}

	public void save() throws SeedpodException {
		makePersist();
	}

	public void setAttributeValue( SlotMap attribute, ArrayList<Object> values ) {
		AVPair binder = new AVPair( this, attribute, values );
		_avPairs.put( attribute.getName(), binder );
	}

	public void setAttributeValue( SlotMap attribute, Object value ) {
		Object formattedValue = reformatUserEntry( attribute, value );
		AVPair binder = getAVPair( attribute );
		if ( binder == null ) {
			binder = new AVPair( this, attribute, formattedValue );
			_avPairs.put( attribute.getName(), binder );
		} else binder.setValue( formattedValue );
	}

	public void setManager( PManager m ) {
		_manager = m;
	}

	// /**
	// * Handles multiple value attribute. The value types are simple types.
	// *
	// * @param att
	// */
	// private ArrayList<Object> setMultiValueAtt( SlotMap att ) throws SQLException {
	// Integer srcOid = getOid();
	// ArrayList<Object> toVals = new ArrayList<Object>();
	// SlotMap fromSlot = null, toSlot = null;
	// String sql;
	// ResultSet multivalSet;
	//
	// String clsTarget = att.getRdbTargetName();
	// ClsMap targetCls = att.getModelMap().getClsMap( clsTarget );
	//
	// if ( targetCls == null ) {
	// System.err.println( "#$#$@# Can't fine target class " + clsTarget );
	// return new ArrayList<Object>();
	// }
	//
	// // Initialize relationship slots two attributes in the actual storage table.
	// // One starts with "FROM" and the other starts with "TO."
	// ArrayList<SlotMap> assocSlotMaps = targetCls.getSlotMaps();
	// for ( SlotMap tmp : assocSlotMaps )
	// if ( tmp.getName().startsWith( "FROM" ) )
	// fromSlot = tmp;
	// else if ( tmp.getName().startsWith( "TO" ) ) toSlot = tmp;
	//
	// // Quit if we don't have both ends of the relationship
	// if ( toSlot == null || fromSlot == null ) return new ArrayList<Object>();
	//
	// /**
	// * Retrieve from the multi value association table query: Select [TO.*] from [targetcls] where [FROM.*] =
	// * thisId; EXAMPLE: select "TO.Symbol.429" from "NOP_Subject.Race.Symbol" where "FROM.NOP_Subject.430" =
	// * [NOP_Subject.oid];
	// */
	// sql = "SELECT " + SqlUtil.QQ( toSlot.getName() ) + " FROM " + SqlUtil.QQ( targetCls.getName() ) + " WHERE " +
	// SqlUtil.QQ( fromSlot.getName() ) + " = " + srcOid;
	// multivalSet = _manager.getRdbConnection().executeQuery( sql );
	// while (multivalSet.next()) {
	// Object val = multivalSet.getObject( 1 );
	// toVals.add( val );
	// }
	// return toVals;
	// }

	private String toFullName( ClsMap cmap, SlotMap smap ) {
		return SqlUtil.QQ( cmap.getName() ) + "." + SqlUtil.QQ( smap.getName() );
	}

	/** save the object to the database * */
	public boolean update() throws SeedpodException {
		if ( isDeleted() ) throw new SeedpodException( "Can't update a deleted object." );
		int result;
		String updateSql = getSqlUpdate();

		try {
			_thingInstance.update();
			result = _manager.getRdbConnection().executeUpdate( updateSql );
		} catch (SQLException e) {
			throw new SeedpodException( e, "Update failed for " + _classType + " " + _oid );
		}

		return result > 0;
	}
}